package ar.edu.itba.it.cg.grupo01.impl.shape;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;

import javax.vecmath.Point2f;
import javax.vecmath.Point3f;
import javax.vecmath.Vector2d;
import javax.vecmath.Vector3f;

import ar.edu.itba.it.cg.grupo01.api.Intersection;
import ar.edu.itba.it.cg.grupo01.api.Ray;
import ar.edu.itba.it.cg.grupo01.api.shape.Shape;
import ar.edu.itba.it.cg.grupo01.impl.IntersectionFactory;
import ar.edu.itba.it.cg.grupo01.impl.Precision;

import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;

public class Triangle extends AbstractShape {

	private Point3f first, second, third;

	// light and texture mapping stuff
	private Point2f firstUV, secondUV, thirdUV;

	public Triangle(final Point3f first, final Point3f second,
			final Point3f third, IntersectionFactory intersectionFactory) {
		super(intersectionFactory);

		this.first = (Point3f) first.clone();
		this.second = (Point3f) second.clone();
		this.third = (Point3f) third.clone();
	}

	public Triangle(Point3f x, Point3f y, Point3f z, Point3f nx, Point3f ny,
			Point3f nz, IntersectionFactory intersectionFactory) {
		super(intersectionFactory);

		this.first = (Point3f) x.clone();
		this.second = (Point3f) y.clone();
		this.third = (Point3f) z.clone();
	}

	public Triangle(Point3f x, Point3f y, Point3f z, float[] u, float[] v,
			IntersectionFactory intersectionFactory) {
		super(intersectionFactory);

		this.first = (Point3f) x.clone();
		this.second = (Point3f) y.clone();
		this.third = (Point3f) z.clone();
		this.firstUV = new Point2f(u[0], v[0]);
		this.secondUV = new Point2f(u[1], v[1]);
		this.thirdUV = new Point2f(u[2], v[2]);

		// this.u = u
		// this.v = v;
	}

	public Triangle(Point3f x, Point3f y, Point3f z, Point3f nx, Point3f ny,
			Point3f nz, float[] u2, float[] v2,
			IntersectionFactory intersectionFactory) {
		super(intersectionFactory);

		this.first = (Point3f) x.clone();
		this.second = (Point3f) y.clone();
		this.third = (Point3f) z.clone();
		this.firstUV = new Point2f(u2[0], v2[0]);
		this.secondUV = new Point2f(u2[1], v2[1]);
		this.thirdUV = new Point2f(u2[2], v2[2]);

		// this.u = u2;
		// this.v = v2;

	}

	@Override
	public String toString() {
		return "Triangle [first=" + first + ", second=" + second + ", third="
				+ third + "]";
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((first == null) ? 0 : first.hashCode());
		result = prime * result + ((second == null) ? 0 : second.hashCode());
		result = prime * result + ((third == null) ? 0 : third.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Triangle other = (Triangle) obj;

		List<Point3f> otherList = Arrays.asList(other.first, other.second,
				other.third);
		final List<Point3f> pointList = Arrays.asList(this.first, this.second,
				this.third);

		Collection<Point3f> filteredPoints = Collections2.filter(otherList,
				new Predicate<Point3f>() {
					@Override
					public boolean apply(Point3f input) {
						for (Point3f point3d : pointList) {
							if (input.epsilonEquals(point3d, Precision.EPSILON)) {
								return true;
							}
						}
						return false;
					}
				});

		return filteredPoints.size() == 3;
	}

	@Override
	public Intersection<? extends Shape> intersects(final Ray ray) {

		Plane plane = getContainingPlane();
		Set<Point3f> intersections = plane.intersects(ray).getIntersections();

		if (intersections.isEmpty())
			return intersectionFactory.emptyIntersection();

		// point is in triangle's plane, now we check its inside triangle

		// there is only one point...
		for (Point3f testPoint : intersections) {

			if (this.contains(testPoint)) {
				return intersectionFactory.createIntersection(intersections,
						this);

			}

		}
		return intersectionFactory.emptyIntersection();

	}

	/**
	 * Returns the containing plane for the triangle should be private
	 * 
	 * @return
	 */
	// TODO: PASARLA A PRIVATE

	public Plane getContainingPlane() {
		// devuelve [a b c d] ax+by+cz+d=0
		Vector3f dir1 = new Vector3f(first);
		Vector3f dir2 = new Vector3f(first);
		dir1.sub(second);
		dir2.sub(third);

		// TODO Crearle un constructor a plane que reciba 3 puntos
		// calculo la normal tomando dos vectores del plano
		Vector3f planeNormal = new Vector3f();
		planeNormal.cross(dir1, dir2);

		return new Plane(third, planeNormal, this.intersectionFactory);
	}

	public boolean contains(Point3f testPoint) {
		if (testPoint.equals(third) || testPoint.equals(first)
				|| testPoint.equals(second)) {
			return true;
		}
		/*
		 * Compute vectors v0 = C - A v1 = B - A v2 = P - A
		 */

		Point2f uvPoint = this.getBaryCoordinates(testPoint);

		float u = uvPoint.x;
		float v = uvPoint.y;

		// Check if point is in triangle
		if (u >= -Precision.EPSILON && v >= -Precision.EPSILON
				&& u + v < 1 + Precision.EPSILON) {
			return true;
		}
		return false;

	}

	public Point3f getFirst() {
		return (Point3f) first.clone();
	}

	public Point3f getSecond() {
		return (Point3f) second.clone();
	}

	public Point3f getThird() {
		return (Point3f) third.clone();
	}

	// @Deprecated
	// public Shape applyTransformation() {
	//
	// Point3f first = new Point3f(this.first);
	// this.transformations.transform(first);
	//
	// Point3f second = new Point3f(this.second);
	// this.transformations.transform(second);
	//
	// Point3f third = new Point3f(this.third);
	// this.transformations.transform(third);
	//
	// return new Triangle(first, second, third, this.intersectionFactory);
	// }

	@Override
	public boolean containedInAABB(AABB box) {

		// return box.intersects((new AABB(this.getMinXYZ(), this.getMaxXYZ(),
		// intersectionFactory)));

		Point3f boxCenter = box.getMid();
		Vector3f boxHalfSize = box.getExtent();

		/********************************************************/

		/* AABB-triangle overlap test code */

		/* by Tomas Akenine-Möller */

		/* Function: int triBoxOverlap(float boxcenter[3], */

		/* float boxhalfsize[3],float triverts[3][3]); */

		/* History: */

		/* 2001-03-05: released the code in its first version */

		/* 2001-06-18: changed the order of the tests, faster */

		/*                                                      */

		/* Acknowledgement: Many thanks to Pierre Terdiman for */

		/* suggestions and discussions on how to optimize code. */

		/* Thanks to David Hunt for finding a ">="-bug! */

		/********************************************************/
		Point3f v0, v1, v2;
		float min, max, fex, fey, fez;
		Vector3f e0, e1, e2;
		Vector3f normal = new Vector3f();

		/* use separating axis theorem to test overlap between triangle and box */

		/* need to test for overlap in these directions: */

		/*
		 * 1) the {x,y,z}-directions (actually, since we use the AABB of the
		 * triangle
		 */

		/* we do not even need to test these) */

		/* 2) normal of the triangle */

		/* 3) crossproduct(edge from tri, {x,y,z}-directin) */

		/* this gives 3x3=9 more tests */

		// float axis[3];

		/* This is the fastest branch on Sun */

		/* move everything so that the boxcenter is in (0,0,0) */

		v0 = (Point3f) first.clone();
		v0.sub(boxCenter);

		v1 = (Point3f) second.clone();
		v1.sub(boxCenter);

		v2 = (Point3f) third.clone();
		v2.sub(boxCenter);

		/* compute triangle edges */

		e0 = new Vector3f(v1.x, v1.y, v1.z);
		e0.sub(v0);
		e1 = new Vector3f(v2.x, v2.y, v2.z);
		e1.sub(v1);
		e2 = new Vector3f(v0.x, v0.y, v0.z);
		e2.sub(v2);

		/* Bullet 3: */

		/* test the 9 tests first (this was faster) */

		fex = Math.abs(e0.x);
		fey = Math.abs(e0.y);
		fez = Math.abs(e0.z);

		if (!axistestX(e0.z, e0.y, fez, fey, v0, v2, boxHalfSize)) {
			return false;
		}

		if (!axistestY(e0.z, e0.x, fez, fex, v0, v2, boxHalfSize)) {
			return false;
		}

		if (!axistestZ(e0.y, e0.x, fey, fex, v1, v2, boxHalfSize)) {
			return false;
		}

		fex = Math.abs(e1.x);
		fey = Math.abs(e1.y);
		fez = Math.abs(e1.z);

		if (!axistestX(e1.z, e1.y, fez, fey, v0, v2, boxHalfSize)) {
			return false;

		}
		if (!axistestY(e1.z, e1.x, fez, fex, v0, v2, boxHalfSize)) {
			return false;

		}
		if (!axistestZ(e1.y, e1.x, fey, fex, v0, v1, boxHalfSize)) {
			return false;
		}

		fex = Math.abs(e2.x);
		fey = Math.abs(e2.y);
		fez = Math.abs(e2.z);

		if (!axistestX(e2.z, e2.y, fez, fey, v0, v1, boxHalfSize)) {
			return false;
		}
		if (!axistestY(e2.z, e2.x, fez, fex, v0, v1, boxHalfSize)) {
			return false;
		}
		if (!axistestZ(e2.y, e2.x, fey, fex, v1, v2, boxHalfSize)) {
			return false;
		}

		/* Bullet 1: */

		/* first test overlap in the {x,y,z}-directions */

		/* find min, max of the triangle each direction, and test for overlap in */

		/* that direction -- this is equivalent to testing a minimal AABB around */

		/* the triangle against the AABB */

		/* test in X-direction */

		if (!minMaxTest(v0.x, v1.x, v2.x, boxHalfSize.x)) {
			return false;
		}
		if (!minMaxTest(v0.y, v1.y, v2.y, boxHalfSize.y)) {
			return false;
		}
		if (!minMaxTest(v0.z, v1.z, v2.z, boxHalfSize.z)) {
			return false;
		}

		min = Math.min(v0.x, Math.min(v1.x, v2.x));
		max = Math.max(v0.x, Math.max(v1.x, v2.x));

		if (min > boxHalfSize.x || max < -boxHalfSize.x) {
			return false;
		}
		min = Math.min(v0.x, Math.min(v1.x, v2.x));
		max = Math.max(v0.x, Math.max(v1.x, v2.x));

		if (min > boxHalfSize.x || max < -boxHalfSize.x) {
			return false;
		}

		/* Bullet 2: */

		/* test if the box intersects the plane of the triangle */

		/* compute plane equation of triangle: normal*x+d=0 */

		normal.cross(e0, e1);

		Plane plane = new Plane(v0, normal, intersectionFactory);

		return plane.containedInAABB(box) ? true : false; /*
														 * box and triangle
														 * overlaps
														 */

	}

	@Override
	public Point3f getMaxXYZ() {
		float xmax, ymax, zmax;

		xmax = ymax = zmax = Float.MIN_VALUE;

		xmax = Math.max(xmax, first.x);
		ymax = Math.max(ymax, first.y);
		zmax = Math.max(zmax, first.z);

		xmax = Math.max(xmax, second.x);
		ymax = Math.max(ymax, second.y);
		zmax = Math.max(zmax, second.z);

		xmax = Math.max(xmax, third.x);
		ymax = Math.max(ymax, third.y);
		zmax = Math.max(zmax, third.z);

		return new Point3f(xmax, ymax, zmax);
	}

	@Override
	public Point3f getMinXYZ() {
		float xmin, ymin, zmin;

		xmin = ymin = zmin = Float.MAX_VALUE;

		xmin = Math.min(xmin, first.x);
		ymin = Math.min(ymin, first.y);
		zmin = Math.min(zmin, first.z);

		xmin = Math.min(xmin, second.x);
		ymin = Math.min(ymin, second.y);
		zmin = Math.min(zmin, second.z);

		xmin = Math.min(xmin, third.x);
		ymin = Math.min(ymin, third.y);
		zmin = Math.min(zmin, third.z);

		return new Point3f(xmin, ymin, zmin);

	}

	private boolean axistestX(float a, float b, float fa, float fb, Point3f v0,
			Point3f v1, Vector3f boxHalfSize) {

		float p0 = a * v0.y - b * v0.z;
		float p1 = a * v1.y - b * v1.z;

		float min, max;

		if (p0 < p1) {
			min = p0;
			max = p1;
		} else {
			min = p1;
			max = p0;
		}

		float rad = fa * boxHalfSize.y + fb * boxHalfSize.z;

		return (min > rad || max < -rad) ? false : true;
	}

	private boolean axistestY(float a, float b, float fa, float fb, Point3f v0,
			Point3f v1, Vector3f boxHalfSize) {

		float p0 = -a * v0.x + b * v0.z;
		float p1 = -a * v1.x + b * v1.z;

		float min, max;

		if (p0 < p1) {
			min = p0;
			max = p1;
		} else {
			min = p1;
			max = p0;
		}

		float rad = fa * boxHalfSize.x + fb * boxHalfSize.z;

		return (min > rad || max < -rad) ? false : true;
	}

	private boolean axistestZ(float a, float b, float fa, float fb, Point3f v0,
			Point3f v1, Vector3f boxHalfSize) {

		float p0 = a * v0.x - b * v0.y;
		float p1 = a * v1.x - b * v1.y;

		float min, max;

		if (p0 < p1) {
			min = p0;
			max = p1;
		} else {
			min = p1;
			max = p0;
		}

		float rad = fa * boxHalfSize.x + fb * boxHalfSize.y;

		return (min > rad || max < -rad) ? false : true;
	}

	private boolean minMaxTest(float v0, float v1, float v2, float boxHalfSize) {
		float min = Math.min(v0, Math.min(v1, v2));
		float max = Math.max(v0, Math.max(v1, v2));

		return (min > boxHalfSize || max < -boxHalfSize) ? false : true;
	}

	private boolean planeBoxOverlap(Vector3f normal, Point3f v0,
			Point3f boxHalfSize) {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public Collection<Point3f> getVertices() {

		List<Point3f> ret = new ArrayList<Point3f>();
		ret.add(this.first);
		ret.add(this.second);
		ret.add(this.third);

		return ret;
	}

	@Override
	public Shape getTransformedShape() {
		return new Triangle(first, second, third, intersectionFactory);

	}

	@Override
	public Vector3f getNormal(Point3f point) {
		return this.getContainingPlane().getNormal(point);
	}

	public Point2f getBaryCoordinates(Point3f point) {
		Vector3f v1 = new Vector3f(third);
		Vector3f v0 = new Vector3f(second);
		Vector3f v2 = new Vector3f(point);
		v2.sub(first);
		v1.sub(first);
		v0.sub(first);

		float dot00 = v0.dot(v0);
		float dot01 = v0.dot(v1);
		float dot02 = v0.dot(v2);
		float dot11 = v1.dot(v1);
		float dot12 = v1.dot(v2);

		// Compute barycentric coordinates
		float invDenom = 1 / (dot00 * dot11 - dot01 * dot01);
		float u = (dot11 * dot02 - dot01 * dot12) * invDenom;
		float v = (dot00 * dot12 - dot01 * dot02) * invDenom;

		if (u < 0 && u > -Precision.EPSILON)
			u = 0;
		if (v < 0 && v > -Precision.EPSILON)
			v = 0;

		return new Point2f(u, v);

	}

	@Override
	public Point2f getUVCoordinates(Point3f point) {
		Point2f uvPoint = getBaryCoordinates(point);

		if (this.firstUV == null || this.secondUV == null
				|| this.thirdUV == null) {
			return uvPoint;
		}

		// interpolate UV using triangle points
		// simple weighted average
		Vector2d v1 = new Vector2d(firstUV);
		Vector2d v2 = new Vector2d(secondUV);
		Vector2d v3 = new Vector2d(thirdUV);

		v1.scale(uvPoint.x);
		v2.scale(uvPoint.y);
		v3.scale(1 - uvPoint.x - uvPoint.y);
		v1.add(v2);
		v1.add(v3);

		return new Point2f(v1);
	}

	@Override
	public Point3f getUVWCoordinates(Point3f point) {
		Point3f result = (Point3f) point.clone();
		result.sub(first);
		return result;
	}
}
